""" sbak/ui.py - sbak.ui - All things related to user input.

    At present, this module only defines event types related to the keyboard,
    such as KeyPressEvent and KeyReleaseEvent.
"""

import operator

from sbak.geometry import Point
from sbak.event import Event

class UIEvent(Event):
    """ Event related to user input in some way. """
    
    signal = "ui"

class KeyboardEvent(UIEvent):
    """ Event related to the keyboard.
    
        Currently this is only used by KeyStateEvent, which is in turn used by
        KeyPressEvent and KeyReleaseEvent. In the future, however,
        KeyboardEvent may be used as a base for other events related to the
        keyboard, such as change in the lock indicators.
    """
    
    signal = "keyboard"

class KeyStateEvent(KeyboardEvent):
    """ Event that indicates some change was made to the state of a key.
    
        KeyStateEvent(subject, key=KEY, code=CODE, shift=SHIFT, alt=ALT,
                        ctrl=CTRL) -> e
        KeyStateEvent(subject, root, key, code, shift, alt, ctrl) -> e
        
        subject:    The subject of the event. At present it is advised
                    for this to be the app object that is currently getting the
                    user input data from the OS.
        
        root:       The root from which this event should be spidered when
                    spidering is used. If spidering is not being used, this
                    should be None.
                    
        key:        A string that indicates the key pressed; see the
                    corresponding attribute below. Defaults to "unknown".
        
        code:       The ordinal key code corresponding to the key. Defaults to
                    0.
        
        shift:      Whether or not a "shift" key is pressed.
        
        alt:        Whether or not an "alt" key is pressed.
        
        ctrl:       Whether or not a "ctrl" key is pressed.
        
        ~~~ Description ~~~
        
        signal = "key_state"
        
        Base for event classes that indicate a change in a keyboard key's
        state. The classes `KeyPress` and `KeyRelease` use this, each changing
        only the signal; the rest of the class behavior stays the same.
        
        ~~~ Attributes ~~~
        
        key:    A string that corresponds to the text of the key. This is
                always lowercase. For character keys other than space, it's the
                character that appears on the key, (ex. "a", "b", "0", "1",
                ",", ".", "`"). For arrow keys, it's the direction of the key,
                (ex. "left"). Some other possible values include "enter",
                "backspace", "tab", "capslock", "lshift", "rshift", "lctrl",
                "rctrl", "insert", "delete" "pageup", "pagedown", and so on.
                (Spacebar appears as "space", not " ". This is to improve
                readability.) If the event corresponds to an unknown key, then
                this will be "unknown".
        
        code:   The ordinal value of the key pressed as a number. If the key
                is "unknown", then this should be 0.
        
        shift:  True if either shift key was pressed when the key was pressed.
        
        alt:    True if either alt key was pressed.
        
        ctrl:   True if either ctrl key was pressed.
        
        ~~~ Comparisons ~~~
        
        KeyPressEvent supports the following event description dict keys. Each
        dict key corresponds to a unique comparison. All comparisons invoked
        must be True in order for the event to match a description.
        
        key:        The `key` attribute of the event. If the value of the `key`
                    key of the event description is equal to the value of the
                    `key` event attribute, the comparison is true. Case and
                    whitespace padding are unimportant for the description key
                    value.
        
        key_in_set: Any collection compatible with the Python built-in `any`
                    function. If any of the keys in the collection is the key
                    of the event, the comparison is true. If `key` and
                    `key_in_set` are both used, then only `key` will really
                    make a difference.
                    
        code:       Like `key`, but for the `code` attribute.
        
        code_in_set:
                    Like `key_in_set`, but for the `code` attribute.
                    
        shift:      The `shift` attribute of the event. If the boolean truth
                    of the `shift` dict key's value is equal to the boolean
                    truth of the `shift` event attribute's value, the
                    comparison is True.
                    
        alt:        Like `shift`, but for `alt`.
        
        ctrl:       Like `shift`, but for `ctrl`.
    """
    
    __slots__ = ["key", "code", "shift", "alt", "ctrl"]
    
    signal = "key_state"
    
    def __init__(self, subject, root=None, key="unknown", code=0, shift=False,
                 alt=False, ctrl=False):
        Event.__init__(self, subject, root)
        for k, v in {   "key" : str(key).strip().lower(),
                        "code" : code,
                        "shift" : bool(shift),
                        "alt" : alt,
                        "ctrl" : ctrl   }.iteritems():
            setattr(self, k, v)
    
    def _gen_compare():
        # Experimental optimization using a closure;
        # each comparison is a sub-function mapped from a key of the same name
        # as the function. The mappings are stored in a dict, which is used in
        # the comparison function later for performing each comparison.
        # Using this technique, several speed-ups can be applied:
        # * Attrgetters can be used for each of the event's main attributes
        # * Each comparison can be a function without having to create/destroy
        #   a function time `compare` is called
        # * The mapping of the functions needs to be created only once
        # * Many names are available from the local scope rather than as
        #   attributes of the instance
        
        getsubject = operator.attrgetter("subject")
        getroot = operator.attrgetter("root")
        getkey = operator.attrgetter('key')
        getcode = operator.attrgetter('code')
        getshift = operator.attrgetter('shift')
        getalt = operator.attrgetter('getalt')
        getctrl = operator.attrgetter('getctrl')
        
        funcs = []
        
        def subject(self, val):
            return getsubject(self) == val
            
        def root(self, val):
            return getroot(self) == val
        
        def key(self, val):
            return getkey(self) == str(val).strip().lower()
        
        def key_in_set(self, val):
            return getkey(self) in (str(key).strip().lower() for key in val)
        
        def code(self, val):
            return getcode(self) == val
        
        def code_in_set(self, val):
            return getcode(self) in val
        
        def shift(self, val):
            return bool(getshift(self)) == bool(val)
        
        def alt(self, val):
            return bool(getalt(self)) == bool(val)
        
        def ctrl(self, val):
            return bool(getctrl(self)) == bool(val)
        
        funcs = (subject, root, key, key_in_set, code, code_in_set, shift,
                 alt, ctrl)
        funcs = dict((func.__name__, func) for func in funcs)
        
        def compare(self, desc):
            """ Compare the event to an event description.
            
                e.compare(desc) -> boolean
                
                See `sbak.event.Event.compare` for more information about the
                purpose of the `compare` method.
                
                See `sbak.ui.KeyStateEvent` for more information about the
                specific comparisons supported by key state change events.
            """
            try:
                return all(funcs[key](self, val)
                           for key, val in desc.iteritems())
            except KeyError:
                raise ValueError("%s is not a valid comparison key for %r"
                                 % (key, self))
            
        return compare
    compare = _gen_compare()
    del _gen_compare

class KeyPressEvent(KeyStateEvent):
    """ Event that indicates a key on the keyboard was pressed down.
    
        This event is a subclass of `sbak.ui.KeyStateEvent` that occurs
        when the user presses a key down.
        
        Note that a KeyPressEvent does not necessarily gauruntee a
        KeyReleaseEvent ocurring later, nor does a KeyReleaseEvent gauruntee
        that a KeyPressEvent ocurred earlier.
        
        signal = "key_press"
    """
    
    signal = "key_press"
    
class KeyReleaseEvent(KeyStateEvent):
    """ Event that indicates a key on the keyboard was released.
    
        This event is a subclass of `sbak.ui.KeyStateEvent` that occurs
        when the user releases a key.
        
        Note that a KeyPressEvent does not necessarily gauruntee a
        KeyReleaseEvent ocurring later, nor does a KeyReleaseEvent gauruntee
        that a KeyPressEvent ocurred earlier.
        
        signal = "key_release"
    """
    
    signal = "key_release"

class MouseEvent(UIEvent):
    """ Event related to the mouse. """
    
    signal = "mouse"

class MouseButtonEvent(MouseEvent):
    """ Event that indicates something happened to a mouse button.
    
        ~~~ Constructor ~~~
        
        MouseButtonEvent(subject[,root][,button][,pos]) -> e
        
        subject:    The subject of the event. At present it is advised
                    for this to be the app object that is currently getting the
                    user input data from the OS.
        
        root:       The root from which this event should be spidered when
                    spidering is used. If spidering is not being used, this
                    should be None.
                    
        button:     The number of button that was affected.
    
        pos:        The position at which the mouse pointer was located at the
                    time the button was affected.
    
        ~~~ Description ~~~
        
        signal = "mouse_button"
        
        This event indicates that something happened in relation to a mouse
        button, either it was pressed or it was released. It is usually one of
        the two subclasses, MousePressEvent or MouseReleaseEvent,
        which occur and not MouseButtonEvent itself.
    
        ~~~ Attributes ~~~
        
        button:     Indicates the button that was affected. This is a number
                    starting at 0 and increasing to the right. 0 is the left
                    button, 1 is the middle button, and 2 is the right button.
        
        pos:        Position on the screen/in the window at which the mouse
                    pointer was located at the time that the button was
                    affected.
                    
        ~~~ Comparisons ~~~
        
        This following comparison keys are supported. All comparisons must be
        true in order for the event to match a description.
        
        button:         The button that was affected. If the button is the
                        button of the event, the comparison is true.
                        
        pos:            The position of the pointer.
        
        button_in_set:  If the affected button is in this set, the comparison
                        is true.
        
        region:         If the pos of the event is in this region, the
                        comparison is true.
    """
    
    signal = "mouse_button"
    
    def __init__(self, subject, root=None, button=0, pos=(0,0)):
        Event.__init__(self, subject, root)
        self.button = int(button)
        self.pos = Point(pos)
        
    def _gen_compare():
        
        getsubject = operator.attrgetter("subject")
        getroot = operator.attrgetter("root")
        getbutton = operator.attrgetter('button')
        getpos = operator.attrgetter('pos')
        
        def subject(self, val):
            return getsubject(self) == val
            
        def root(self, val):
            return getroot(self) == val
        
        def button(self, val):
            return getbutton(self) == val
        
        def button_in_set(self, val):
            return getbutton(self) in val
        
        def pos(self, val):
            return getpos(self) == val
        
        def region(self, val):
            return Rect(val).contains(getpos(self))
            
        funcs = (subject, root, button, button_in_set, pos, region)
        funcs = dict((func.__name__, func) for func in funcs)
        
        def compare(self, desc):
            """ Compare the event to an event description.
            
                e.compare(desc) -> boolean
                
                See `sbak.event.Event.compare` for more information about the
                purpose of the `compare` method.
                
                See `sbak.ui.MouseButtonEvent` for more information about the
                specific comparisons supported by mouse button state change
                events.
            """
            try:
                return all(funcs[key](self, val)
                           for key, val in desc.iteritems())
            except KeyError:
                raise ValueError("%s is not a valid comparison key for %r"
                                 % (key, self))
            
        return compare
    compare = _gen_compare()
    del _gen_compare

class MousePressEvent(MouseButtonEvent):
    """ Event that indicates that a mouse button was pressed.
    
        See `MouseButtonEvent` for more information about what attributes and
        comparison keys are available.
        
        signal = "mouse_press"
    """
    
    signal = "mouse_press"

class MouseReleaseEvent(MouseButtonEvent):
    """ Event that indicates that a mouse button was released.
    
        See `MouseButtonEvent` for more information about what attributes and
        comparison keys are available.
        
        signal = "mouse_release"
    """
    
    signal = "mouse_release"
    
class MouseMoveEvent(MouseEvent):
    """ Event that indicates that the mouse pointer was moved.
    
        ~~~ Constructor ~~~
        
        MouseMove(subject[, root][, pos][, rel][, buttons])
        
        ...:    ...
        
        pos:        Position on screen to which the mouse pointer was moved.
                    Should be a Point object.
        
        rel:        Movement of the mouse pointer; in other words, the
                    position to which it moved relative to its previous
                    position. Should also be a Point object.
        
        buttons:    A collection of numbers indicating the buttons that were
                    pressed at the time of the mouse movement.
                    (0 = left, 1 = middle, 2 = right)
                    If no buttons are pressed, then the collection will be
                    empty (length will be 0).
    
        TODO: Comparisons
    
    """
    
    signal = "mouse_move"
    
    def __init__(self, subject, root=None, pos=(0,0), rel=(0,0), buttons=()):
        Event.__init__(self, subject, root)
        self.buttons = tuple(int(button) for button in buttons)
        self.pos = Point(pos)
        
    # TODO: Comparisons

class MouseScrollEvent(MouseEvent):
    """ Event that indicates the mouse wheel was scrolled.
    
        ~~~ Constructor ~~~
        
        MouseScrollEvent(subject[, root][, val][, pos]) -> e
        
        ...:    ...
        
        val:    The amount scrolled. This will be either +1 or -1.
        
        pos:    Position at which the mouse pointer was located at the time of
                the scroll.
                
        TODO: Comparisons
    """
    
    signal = "mouse_scroll"
    
    def __init__(self, subject, root=None, val=1, pos=(0,0)):
        Event.__init__(self, subject, root)
        self.val = +1 if val >= 1 else -1
        self.pos = Point(pos)
        
    # TODO: Comparisons